نظرة متعمقة في React's useSyncExternalStore hook لمزامنة مخازن البيانات الخارجية، بما في ذلك استراتيجيات التنفيذ، واعتبارات الأداء، وحالات الاستخدام المتقدمة.
React useSyncExternalStore: إتقان مزامنة المخزن الخارجي
في تطبيقات React الحديثة، تعتبر إدارة الحالة بفعالية أمرًا بالغ الأهمية. بينما توفر React حلولًا مدمجة لإدارة الحالة مثل useState و useReducer، فإن التكامل مع مصادر البيانات الخارجية أو مكتبات إدارة الحالة التابعة لجهات خارجية يتطلب نهجًا أكثر تطوراً. هذا هو المكان الذي يأتي فيه useSyncExternalStore.
ما هو useSyncExternalStore؟
useSyncExternalStore هو React hook تم تقديمه في React 18 الذي يسمح لك بالاشتراك في مصادر البيانات الخارجية والقراءة منها بطريقة متوافقة مع العرض المتزامن. هذا مهم بشكل خاص عند التعامل مع البيانات التي لا تتم إدارتها مباشرة بواسطة React، مثل:
- مكتبات إدارة الحالة التابعة لجهات خارجية: Redux، Zustand، Jotai، إلخ.
- واجهات برمجة تطبيقات المتصفح:
localStorage،IndexedDB، إلخ. - مصادر البيانات الخارجية: أحداث مرسلة من الخادم، WebSockets، إلخ.
قبل useSyncExternalStore، يمكن أن يؤدي مزامنة المتاجر الخارجية إلى التمزق والتناقضات، خاصة مع ميزات العرض المتزامن لـ React. يعالج هذا الـ hook هذه المشكلات من خلال توفير طريقة موحدة وعالية الأداء لتوصيل البيانات الخارجية بمكونات React الخاصة بك.
لماذا useSyncExternalStore؟ الفوائد والمزايا
يوفر استخدام useSyncExternalStore العديد من المزايا الرئيسية:
- السلامة المتزامنة: يضمن أن يعرض المكون الخاص بك دائمًا عرضًا متسقًا للمخزن الخارجي، حتى أثناء عمليات العرض المتزامنة. هذا يمنع مشكلات التمزق حيث قد تعرض أجزاء من واجهة المستخدم الخاصة بك بيانات غير متناسقة.
- الأداء: مُحسَّن للأداء، مما يقلل من عمليات إعادة العرض غير الضرورية. إنه يستفيد من الآليات الداخلية لـ React للاشتراك بكفاءة في التغييرات وتحديث المكون فقط عند الضرورة.
- واجهة برمجة تطبيقات موحدة: يوفر واجهة برمجة تطبيقات متسقة ويمكن التنبؤ بها للتفاعل مع المتاجر الخارجية، بغض النظر عن التنفيذ الأساسي.
- تقليل التعليمات البرمجية القياسية: يبسط عملية الاتصال بالمتاجر الخارجية، مما يقلل من مقدار التعليمات البرمجية المخصصة التي تحتاج إلى كتابتها.
- التوافق: يعمل بسلاسة مع مجموعة واسعة من مصادر البيانات الخارجية ومكتبات إدارة الحالة.
كيف يعمل useSyncExternalStore: نظرة متعمقة
يأخذ الـ hook useSyncExternalStore ثلاث وسيطات:
subscribe(callback: () => void): () => void: دالة تسجل رد نداء ليتم إعلامه عند تغيير المتجر الخارجي. يجب أن تُرجع دالة لإلغاء الاشتراك. هذه هي الطريقة التي تتعلم بها React متى كان المتجر لديه بيانات جديدة.getSnapshot(): T: دالة تُرجع لقطة من البيانات من المتجر الخارجي. يجب أن تكون هذه اللقطة قيمة بسيطة وغير قابلة للتغيير يمكن لـ React استخدامها لتحديد ما إذا كانت البيانات قد تغيرت.getServerSnapshot?(): T(اختياري): دالة تُرجع اللقطة الأولية للبيانات على الخادم. يتم استخدام هذا للعرض من جانب الخادم (SSR) لضمان الاتساق بين الخادم والعميل. إذا لم يتم توفيره، فستستخدم ReactgetSnapshot()أثناء العرض من جانب الخادم، والذي قد لا يكون مثاليًا لجميع السيناريوهات.
إليك تفصيل لكيفية عمل هذه الوسيطات معًا:
- عندما يتم تحميل المكون، يستدعي
useSyncExternalStoreالدالةsubscribeلتسجيل رد نداء. - عندما يتغير المتجر الخارجي، فإنه يستدعي رد النداء المسجل من خلال
subscribe. - يخبر رد النداء React أن المكون يحتاج إلى إعادة عرضه.
- أثناء العرض، يستدعي
useSyncExternalStoreالدالةgetSnapshotللحصول على أحدث البيانات من المتجر الخارجي. - تقارن React اللقطة الحالية باللقطة السابقة. إذا كانت مختلفة، يتم تحديث المكون بالبيانات الجديدة.
- عندما يتم إلغاء تحميل المكون، يتم استدعاء دالة إلغاء الاشتراك التي تم إرجاعها بواسطة
subscribeلمنع تسرب الذاكرة.
مثال على التنفيذ الأساسي: التكامل مع localStorage
دعنا نوضح كيفية استخدام useSyncExternalStore بمثال بسيط: قراءة وكتابة قيمة إلى localStorage.
import { useSyncExternalStore } from 'react';
function getLocalStorageItem(key: string): string | null {
try {
return localStorage.getItem(key);
} catch (error) {
console.error("Error accessing localStorage:", error);
return null; // Handle potential errors like `localStorage` being unavailable.
}
}
function useLocalStorage(key: string): [string | null, (value: string) => void] {
const subscribe = (callback: () => void) => {
window.addEventListener('storage', callback);
return () => window.removeEventListener('storage', callback);
};
const getSnapshot = () => getLocalStorageItem(key);
const serverSnapshot = () => null; // Or a default value if appropriate for your SSR setup
const value = useSyncExternalStore(subscribe, getSnapshot, serverSnapshot);
const setValue = (newValue: string) => {
try {
localStorage.setItem(key, newValue);
// Dispatch a storage event on the current window to trigger updates in other tabs.
window.dispatchEvent(new StorageEvent('storage', {
key: key,
newValue: newValue,
storageArea: localStorage,
} as StorageEventInit));
} catch (error) {
console.error("Error setting localStorage:", error);
}
};
return [value, setValue];
}
function MyComponent() {
const [name, setName] = useLocalStorage('name');
return (
<div>
<p>Hello, {name || 'World'}</p>
<input
type="text"
value={name || ''}
onChange={(e) => setName(e.target.value)}
/>
</div>
);
}
export default MyComponent;
شرح:
getLocalStorageItem: دالة مساعدة لاسترداد القيمة بأمان منlocalStorage، والتعامل مع الأخطاء المحتملة.useLocalStorage: Hook مخصص يغلف منطق التفاعل معlocalStorageباستخدامuseSyncExternalStore.subscribe: يستمع إلى حدث'storage'، الذي يتم تشغيله عند تعديلlocalStorageفي علامة تبويب أو نافذة أخرى. بشكل حاسم، نرسل حدث تخزين بعد تعيين قيمة جديدة لتشغيل التحديثات بشكل صحيح في نفس النافذة.getSnapshot: تُرجع القيمة الحالية منlocalStorage.serverSnapshot: تُرجعnull(أو قيمة افتراضية) للعرض من جانب الخادم.setValue: تحديث القيمة فيlocalStorageوإرسال حدث تخزين للإشارة إلى علامات التبويب الأخرى.MyComponent: مكون بسيط يستخدم الـ hookuseLocalStorageلعرض وتحديث اسم.
اعتبارات مهمة لـ localStorage:
- معالجة الأخطاء: قم دائمًا بتضمين الوصول إلى
localStorageفي كتلtry...catchللتعامل مع الأخطاء المحتملة، مثل عندما يتم تعطيلlocalStorageأو عدم توفره (على سبيل المثال، في وضع التصفح الخاص). - أحداث التخزين: يتم تشغيل حدث
'storage'فقط عند تعديلlocalStorageفي علامة تبويب أو نافذة *أخرى*، وليس في نفس النافذة. لذلك، نرسلStorageEventجديد يدويًا بعد تعيين قيمة. - تسلسل البيانات: يخزن
localStorageسلاسل فقط. قد تحتاج إلى تسلسل وإلغاء تسلسل هياكل البيانات المعقدة باستخدامJSON.stringifyوJSON.parse. - الأمان: كن على دراية بالبيانات التي تخزنها في
localStorage، حيث يمكن الوصول إليها من خلال كود JavaScript على نفس النطاق. لا ينبغي تخزين المعلومات الحساسة فيlocalStorage.
حالات الاستخدام المتقدمة والأمثلة
1. التكامل مع Zustand (أو مكتبة إدارة الحالة الأخرى)
يعد دمج useSyncExternalStore مع مكتبة إدارة الحالة العالمية مثل Zustand حالة استخدام شائعة. إليك مثال:
import { useSyncExternalStore } from 'react';
import { create } from 'zustand';
interface BearState {
bears: number
increase: (by: number) => void
}
const useStore = create<BearState>((set) => ({
bears: 0,
increase: (by) => set((state) => ({ bears: state.bears + by }))
}))
function BearCounter() {
const bears = useSyncExternalStore(
useStore.subscribe,
useStore.getState,
() => ({ bears: 0, increase: () => {} }) // Server snapshot, provide default state
).bears
return <h1>{bears} bears around here!</h1>
}
function Controls() {
const increase = useStore(state => state.increase)
return (<button onClick={() => increase(1)}>one bear</button>)
}
export { BearCounter, Controls }
شرح:
- نحن نستخدم Zustand لإدارة الحالة العالمية
useStore.subscribe: تشترك هذه الدالة في مخزن Zustand وستؤدي إلى إعادة العرض عند تغيير حالة المخزن.useStore.getState: تُرجع هذه الدالة الحالة الحالية لمخزن Zustand.- توفر المعلمة الثالثة حالة افتراضية للعرض من جانب الخادم (SSR)، مما يضمن عرض المكون بشكل صحيح على الخادم قبل أن يتولى JavaScript من جانب العميل المسؤولية.
- يحصل المكون على عدد الدببة باستخدام
useSyncExternalStoreويعرضه. - يوضح المكون
Controlsكيفية استخدام أداة تعيين Zustand.
2. التكامل مع أحداث مرسلة من الخادم (SSE)
يمكن استخدام useSyncExternalStore لتحديث المكونات بكفاءة بناءً على بيانات في الوقت الفعلي من خادم باستخدام أحداث مرسلة من الخادم (SSE).
import { useSyncExternalStore, useState, useEffect, useCallback } from 'react';
function useSSE(url: string) {
const [data, setData] = useState<any>(null);
const [eventSource, setEventSource] = useState<EventSource | null>(null);
useEffect(() => {
const newEventSource = new EventSource(url);
setEventSource(newEventSource);
newEventSource.onmessage = (event) => {
try {
const parsedData = JSON.parse(event.data);
setData(parsedData);
} catch (error) {
console.error("Error parsing SSE data:", error);
}
};
newEventSource.onerror = (error) => {
console.error("SSE error:", error);
};
return () => {
newEventSource.close();
setEventSource(null);
};
}, [url]);
const subscribe = useCallback((callback: () => void) => {
if (eventSource) {
eventSource.addEventListener('message', callback);
}
return () => {
if (eventSource) {
eventSource.removeEventListener('message', callback);
}
};
}, [eventSource]);
const getSnapshot = useCallback(() => data, [data]);
const serverSnapshot = useCallback(() => null, []);
const value = useSyncExternalStore(subscribe, getSnapshot, serverSnapshot);
return value;
}
function RealTimeDataComponent() {
const realTimeData = useSSE('/api/sse'); // Replace with your SSE endpoint
if (!realTimeData) {
return <p>Loading...</p>;
}
return <div><p>Real-time Data: {JSON.stringify(realTimeData)}</p></div>;
}
export default RealTimeDataComponent;
شرح:
useSSE: هوك مخصص يؤسس اتصال SSE بعنوان URL معين.subscribe: يضيف مستمع حدث إلى كائنEventSourceليتم إعلامه بالرسائل الجديدة من الخادم. يستخدمuseCallbackلضمان عدم إعادة إنشاء دالة رد النداء في كل عرض.getSnapshot: تُرجع أحدث البيانات المستلمة من دفق SSE.serverSnapshot: تُرجعnullللعرض من جانب الخادم.RealTimeDataComponent: مكون يستخدم الـ hookuseSSEلعرض البيانات في الوقت الفعلي.
3. التكامل مع IndexedDB
قم بمزامنة مكونات React مع البيانات المخزنة في IndexedDB باستخدام useSyncExternalStore.
import { useSyncExternalStore, useState, useEffect, useCallback } from 'react';
interface IDBData {
id: number;
name: string;
}
async function getAllData(): Promise<IDBData[]> {
return new Promise((resolve, reject) => {
const request = indexedDB.open('myDataBase', 1); // Replace with your database name and version
request.onerror = (event) => {
console.error("IndexedDB open error:", event);
reject(event);
};
request.onsuccess = (event) => {
const db = (event.target as IDBRequest).result as IDBDatabase;
const transaction = db.transaction(['myDataStore'], 'readonly'); // Replace with your store name
const objectStore = transaction.objectStore('myDataStore');
const getAllRequest = objectStore.getAll();
getAllRequest.onsuccess = (event) => {
const data = (event.target as IDBRequest).result as IDBData[];
resolve(data);
};
getAllRequest.onerror = (event) => {
console.error("IndexedDB getAll error:", event);
reject(event);
};
};
request.onupgradeneeded = (event) => {
const db = (event.target as IDBRequest).result as IDBDatabase;
db.createObjectStore('myDataStore', { keyPath: 'id' });
};
});
}
function useIndexedDBData(): IDBData[] | null {
const [data, setData] = useState<IDBData[] | null>(null);
const [dbInitialized, setDbInitialized] = useState(false);
useEffect(() => {
const initDB = async () => {
try{
await getAllData();
setDbInitialized(true);
} catch (e) {
console.error("IndexedDB initialization failed", e);
}
}
initDB();
}, []);
const subscribe = useCallback((callback: () => void) => {
// Debounce the callback to prevent excessive re-renders.
let timeoutId: NodeJS.Timeout;
const debouncedCallback = () => {
clearTimeout(timeoutId);
timeoutId = setTimeout(callback, 50); // Adjust the debounce delay as needed
};
const handleVisibilityChange = () => {
// Re-fetch data when the tab becomes visible again
if (document.visibilityState === 'visible') {
debouncedCallback();
}
};
window.addEventListener('focus', debouncedCallback);
document.addEventListener('visibilitychange', handleVisibilityChange);
return () => {
window.removeEventListener('focus', debouncedCallback);
document.removeEventListener('visibilitychange', handleVisibilityChange);
clearTimeout(timeoutId);
};
}, []);
const getSnapshot = useCallback(() => {
// Fetch the latest data from IndexedDB every time getSnapshot is called
getAllData().then(newData => setData(newData));
return data;
}, [data]);
const serverSnapshot = useCallback(() => null, []);
return useSyncExternalStore(subscribe, getSnapshot, serverSnapshot);
}
function IndexedDBComponent() {
const data = useIndexedDBData();
if (!data) {
return <p>Loading data from IndexedDB...</p>;
}
return (
<div>
<h2>Data from IndexedDB:</h2>
<ul>
{data.map((item) => (
<li key={item.id}>{item.name} (ID: {item.id})</li>
))}
</ul>
</div>
);
}
export default IndexedDBComponent;
شرح:
getAllData: دالة غير متزامنة تسترد جميع البيانات من مخزن IndexedDB.useIndexedDBData: هوك مخصص يستخدمuseSyncExternalStoreللاشتراك في التغييرات في IndexedDB.subscribe: يقوم بإعداد مستمعين لتغييرات الرؤية والتركيز لتحديث البيانات من IndexedDB ويستخدم دالة منع الارتداد لتجنب التحديثات المفرطة.getSnapshot: يجلب اللقطة الحالية عن طريق استدعاءgetAllData()ثم إرجاعdataمن الحالة.serverSnapshot: تُرجعnullللعرض من جانب الخادم.IndexedDBComponent: مكون يعرض البيانات من IndexedDB.
اعتبارات مهمة لـ IndexedDB:
- العمليات غير المتزامنة: التفاعلات مع IndexedDB غير متزامنة، لذلك تحتاج إلى التعامل مع الطبيعة غير المتزامنة لاسترجاع البيانات والتحديثات بعناية.
- معالجة الأخطاء: قم بتنفيذ معالجة قوية للأخطاء للتعامل بأمان مع المشكلات المحتملة في الوصول إلى قاعدة البيانات، مثل عدم العثور على قاعدة البيانات أو أخطاء الأذونات.
- إصدار قاعدة البيانات: قم بإدارة إصدارات قاعدة البيانات بعناية باستخدام حدث
onupgradeneededلضمان توافق البيانات مع تطور تطبيقك. - الأداء: يمكن أن تكون عمليات IndexedDB بطيئة نسبيًا، خاصة بالنسبة لمجموعات البيانات الكبيرة. قم بتحسين الاستعلامات والفهرسة لتحسين الأداء.
اعتبارات الأداء
في حين أن useSyncExternalStore مُحسَّن للأداء، إلا أن هناك بعض الاعتبارات التي يجب وضعها في الاعتبار:
- تقليل تغييرات اللقطة: تأكد من أن الدالة
getSnapshotتُرجع لقطة جديدة فقط عندما تتغير البيانات بالفعل. تجنب إنشاء كائنات أو مصفوفات جديدة دون داع. ضع في اعتبارك استخدام تقنيات المذكرة لتحسين إنشاء اللقطة. - تحديثات الدُفعات: إذا كان ذلك ممكنًا، قم بتجميع التحديثات إلى المتجر الخارجي لتقليل عدد عمليات إعادة العرض. على سبيل المثال، إذا كنت تقوم بتحديث خصائص متعددة في المتجر، فحاول تحديثها جميعًا في معاملة واحدة.
- منع الارتداد/الخانق: إذا كان المتجر الخارجي يتغير بشكل متكرر، ففكر في منع ارتداد أو تقييد التحديثات لمكون React. يمكن أن يمنع هذا عمليات إعادة العرض المفرطة ويحسن الأداء. هذا مفيد بشكل خاص مع المتاجر المتقلبة مثل تغيير حجم نافذة المتصفح.
- المقارنة السطحية: تأكد من أنك تُرجع قيمًا أولية أو كائنات غير قابلة للتغيير في
getSnapshotبحيث يمكن لـ React تحديد بسرعة ما إذا كانت البيانات قد تغيرت باستخدام مقارنة سطحية. - التحديثات المشروطة: في الحالات التي يتغير فيها المتجر الخارجي بشكل متكرر ولكن المكون الخاص بك يحتاج فقط إلى التفاعل مع تغييرات معينة، فكر في تنفيذ تحديثات مشروطة داخل الدالة
subscribeلتجنب عمليات إعادة العرض غير الضرورية.
المزالق الشائعة واستكشاف الأخطاء وإصلاحها
- مشكلات التمزق: إذا كنت لا تزال تواجه مشكلات في التمزق بعد استخدام
useSyncExternalStore، فتحقق جيدًا من أن الدالةgetSnapshotتُرجع عرضًا متسقًا للبيانات وأن الدالةsubscribeتُبلغ React بالتغييرات بشكل صحيح. تأكد من أنك لا تقوم بتغيير البيانات مباشرة داخل الدالةgetSnapshot. - حلقات لانهائية: يمكن أن تحدث حلقة لانهائية إذا كانت الدالة
getSnapshotتُرجع دائمًا قيمة جديدة، حتى عندما لم تتغير البيانات. يمكن أن يحدث هذا إذا كنت تقوم بإنشاء كائنات أو مصفوفات جديدة دون داع. تأكد من أنك تُرجع نفس القيمة إذا لم تتغير البيانات. - العرض من جانب الخادم المفقود: إذا كنت تستخدم العرض من جانب الخادم، فتأكد من توفير دالة
getServerSnapshotلضمان عرض المكون بشكل صحيح على الخادم. يجب أن تُرجع هذه الدالة الحالة الأولية للمتجر الخارجي. - إلغاء الاشتراك غير الصحيح: تأكد دائمًا من إلغاء الاشتراك بشكل صحيح من المتجر الخارجي داخل الدالة التي تُرجعها
subscribe. يمكن أن يؤدي عدم القيام بذلك إلى تسرب الذاكرة. - الاستخدام غير الصحيح مع الوضع المتزامن: تأكد من أن متجرك الخارجي متوافق مع الوضع المتزامن. تجنب إجراء تغييرات على المتجر الخارجي أثناء قيام React بالعرض. يجب أن تكون التغييرات متزامنة وقابلة للتنبؤ.
الخلاصة
useSyncExternalStore هي أداة قوية لمزامنة مكونات React مع مخازن البيانات الخارجية. من خلال فهم كيفية عملها واتباع أفضل الممارسات، يمكنك التأكد من أن مكوناتك تعرض بيانات متسقة وحديثة، حتى في سيناريوهات العرض المتزامن المعقدة. يبسط هذا الـ hook التكامل مع مصادر البيانات المختلفة، من مكتبات إدارة الحالة التابعة لجهات خارجية إلى واجهات برمجة تطبيقات المتصفح ودفقات البيانات في الوقت الفعلي، مما يؤدي إلى تطبيقات React أكثر قوة وذات أداء أفضل. تذكر دائمًا التعامل مع الأخطاء المحتملة وتحسين الأداء وإدارة الاشتراكات بعناية لتجنب المزالق الشائعة.